/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is Forte for Java, Community Edition. The Initial * Developer of the Original Code is Sun Microsystems, Inc. Portions * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved. */ package org.openide.filesystems; import java.io.*; import java.util.*; import java.lang.ref.*; import javax.swing.event.EventListenerList; import org.openide.TopManager; import org.openide.util.Utilities; import org.openide.util.enum.*; /** Implementation of the file that simplyfies common * tasks with hierarchy of objects for AbstractFileObject and MultiFileObject. * * @author Jaroslav Tulach, */ abstract class AbstractFolder extends FileObject { /** empty array */ private static final AbstractFileObject[] EMPTY_ARRAY = new AbstractFileObject[0]; /** default extension separator */ private static final char EXT_SEP = '.'; /** empty hash map to mark that we are initialized */ private static final HashMap EMPTY = new HashMap (0); /** file system */ private FileSystem system; /** name of the file (only name and extension) */ protected String name; /** strong reference to parent (can be null for root) */ protected final AbstractFolder parent; /** Stores the system name of the file system to test * validity later. */ protected String systemName; /** list of children */ private String[] children; /** map that assignes file object to names. (String, Reference (AbstractFileObject)) * @associates WeakReference*/ private HashMap map; /** listeners */ private EventListenerList listeners; /** Reference to full name of the file */ protected Reference fullName; /** Constructor. Takes reference to file system this file belongs to. * * @param fs the file system * @param parent the parent object (folder) * @param name name of the object (e.g. <code>filename.ext</code>) */ public AbstractFolder( FileSystem fs, AbstractFolder parent, String name ) { this.system = fs; this.parent = parent; this.name = name; this.systemName = fs.getSystemName (); } /** To obtain real full name. */ final String getNameExt () { // used in MultiFileSystem return name; } /* Get the name without extension of this file. * * @return name of the file (in its enclosing folder) */ public final String getName () { int i = name.lastIndexOf ('.'); return i == -1 || isFolder () ? name : name.substring (0, i); } /* Get the extension of this file. * This is the string after the last dot of the full name, if any. * * @return extension of the file (if any) or empty string if there is none */ public final String getExt () { int i = name.lastIndexOf ('.') + 1; return i == 0 || i == name.length () ? "" : name.substring (i); // NOI18N } /** Get fully-qualified filename. Does so by walking through all folders * to the root of the file system. Separates files with provided <code>separatorChar</code>. * The extension, if present, is separated from the basename with <code>extSepChar</code>. * * @param separatorChar char to separate folders and files * @param extSepChar char to separate extension * @return the fully-qualified filename */ public final String getPackageNameExt (char separatorChar, char extSepChar) { StringBuffer sb = new StringBuffer (50); constructName (sb, separatorChar, extSepChar); return sb.toString (); } /** Helper method to construct file name for a file system. * @param sb buffer to add text to * @param sepChar char to separate characters from * @param extChar char to use to separate extension * @return true if something has been added to the list */ private boolean constructName (StringBuffer sb, char sepChar, char extChar) { if (parent == null) { return false; } if (parent.constructName (sb, sepChar, extChar)) { sb.append (sepChar); } sb.append (name.replace (EXT_SEP, extChar)); return true; } /* Getter for the right file system */ public final FileSystem getFileSystem () { return system; } // // Info // /* Test whether this object is the root folder. * The root should always be a folder. * @return true if the object is the root of a file system */ public final boolean isRoot () { return parent == null; } /* Test whether the file is valid. The file can be invalid if it has been deserialized * and the file no longer exists on disk; or if the file has been deleted. * * @return true if the file object is valid */ public final boolean isValid () { // valid if (parent == null) { return true; } // can use == because the system name uses intern () method of string return getFileSystem ().getSystemName () == systemName; } // // List // /* Get parent folder. * The returned object will satisfy {@link #isFolder}. * * @return the parent folder or <code>null</code> if this object {@link #isRoot}. */ public final FileObject getParent () { return parent; } /* Get all children of this folder (files and subfolders). If the file does not have children * (does not exist or is not a folder) then an empty array should be returned. No particular order is assumed. * * @return array of direct children * @see #getChildren(boolean) * @see #getFolders * @see #getData */ public final synchronized FileObject[] getChildren () { check (); if (children == null) { return new FileObject[0]; } int size = children.length; FileObject[] arr = new FileObject[size]; for (int i = 0; i < size; i++) { arr[i] = getChild (children[i]); } return arr; } /** Tries to find a resource. * @param en enumeration of strings to scan * @return found object or null */ final FileObject find (Enumeration en) { AbstractFolder fo = this; while (fo != null && en.hasMoreElements ()) { // try to go on // lock to provide safety for getChild synchronized (fo) { // JST: Better to call the check only here, // than in getChild, than it is not called // so often. fo.check (); fo = fo.getChild ((String)en.nextElement ()); } } // no next requirements or not found return fo; } /** Tries to find a resource if it exists in memory. * @param en enumeration of strings to scan * @return found object or null */ final FileObject findIfExists (Enumeration en) { AbstractFolder fo = this; while (fo != null && en.hasMoreElements ()) { if (fo.map == null) { // this object is not initialized yet return null; } // try to go on // lock to provide safety for getChild synchronized (fo) { // JST: Better to call the check only here, // than in getChild, than it is not called // so often. fo.check (); fo = fo.getChild ((String)en.nextElement ()); } } // no next requirements or not found return fo; } /** Finds one child for given name . * @param name the name of the child * @return the file object or null if it does not exist */ protected final AbstractFolder getChild (String name) { Reference r = (Reference)map.get (name); if (r == null) { return null; } AbstractFolder fo = (AbstractFolder)(r.get ()); if (fo == null) { // object does not exist => have to recreate it fo = createFile (name); map.put (name, new WeakReference (fo)); } return fo; } /** Obtains enumeration of all existing subfiles. */ final synchronized AbstractFileObject[] subfiles () { if (map == null) { return EMPTY_ARRAY; } Iterator it = map.values ().iterator (); LinkedList ll = new LinkedList (); while (it.hasNext ()) { Reference r = (Reference)it.next (); if (r == null) { continue; } AbstractFolder fo = (AbstractFolder)r.get (); if (fo != null && (!fo.isFolder () || fo.map != null)) { // if the file object exists and either is not folder (then // we have to check the time) or it is folder and it has // some children // => use it ll.add (fo); } } return (AbstractFileObject[])ll.toArray (EMPTY_ARRAY); } /* Retrieve file contained in this folder by name. * <em>Note</em> that no file is created on disk. * @param name basename of the file (in this folder) * @param ext extension of the file; <CODE>null</CODE> or <code>""</code> * if the file should have no extension * @return the object representing this file or <CODE>null</CODE> if the file * does not exist * @exception IllegalArgumentException if <code>this</code> is not a folder */ public final synchronized FileObject getFileObject (String name, String ext) { check (); if (ext == null || ext.equals ("")) { // NOI18N return getChild (name); } else { return getChild (name + EXT_SEP + ext); } } /* Refresh the contents of a folder. Rescans the list of children names. */ public final void refresh(boolean expected) { refresh (null, null, true, expected); } // // Listeners section // /* Add new listener to this object. * @param l the listener */ public final void addFileChangeListener (FileChangeListener fcl) { if (listeners == null) { synchronized (EMPTY_ARRAY) { if (listeners == null) { listeners = new EventListenerList (); } } } listeners.add (FileChangeListener.class, fcl); } /* Remove listener from this object. * @param l the listener */ public final void removeFileChangeListener (FileChangeListener fcl) { if (listeners != null) { listeners.remove (FileChangeListener.class, fcl); } } /** Fires event */ protected final void fileDeleted0(FileEvent fileevent) { super.fireFileDeletedEvent(listeners (), fileevent); if(fileevent.getFile().equals(this) && parent != null) { FileEvent ev = new FileEvent(parent, fileevent.getFile()); ev.setExpected (fileevent.isExpected ()); parent.fileDeleted0(ev); } } /** Fires event */ protected final void fileCreated0(FileEvent fileevent, boolean flag) { if(flag) super.fireFileDataCreatedEvent(listeners (), fileevent); else super.fireFileFolderCreatedEvent(listeners (), fileevent); if(fileevent.getFile().equals(this) && parent != null) { FileEvent ev = new FileEvent(parent, fileevent.getFile()); ev.setExpected (fileevent.isExpected ()); parent.fileCreated0 (ev, flag); } } /** Fires event */ protected final void fileChanged0 (FileEvent fileevent) { super.fireFileChangedEvent(listeners (), fileevent); if(fileevent.getFile().equals(this) && parent != null) { FileEvent ev = new FileEvent(parent, fileevent.getFile()); ev.setExpected (fileevent.isExpected ()); parent.fileChanged0 (ev); } } /** Fires event */ protected final void fileRenamed0 (FileRenameEvent filerenameevent) { super.fireFileRenamedEvent(listeners (), filerenameevent); if(filerenameevent.getFile().equals(this) && parent != null) { FileRenameEvent ev = new FileRenameEvent( parent, filerenameevent.getFile(), filerenameevent.getName(), filerenameevent.getExt() ); ev.setExpected (filerenameevent.isExpected ()); parent.fileRenamed0 (ev); } } /** Fires event */ protected final void fileAttributeChanged0 (FileAttributeEvent fileattributeevent) { super.fireFileAttributeChangedEvent(listeners (), fileattributeevent); if(fileattributeevent.getFile().equals(this) && parent != null) { FileAttributeEvent ev = new FileAttributeEvent( parent, fileattributeevent.getFile(), fileattributeevent.getName(), fileattributeevent.getOldValue(), fileattributeevent.getNewValue() ); ev.setExpected (fileattributeevent.isExpected ()); parent.fileAttributeChanged0 (ev); } } /** @return true if there is a listener */ protected final boolean hasListeners () { return listeners != null && listeners.getListenerList ().length != 0; } /** @return true if this folder or its parent have listeners */ protected final boolean hasAtLeastOneListeners () { return hasListeners () || (parent != null && parent.hasListeners ()); } /** @return enumeration of all listeners. */ private final Enumeration listeners () { if (listeners == null) { return EmptyEnumeration.EMPTY; } else { return new FilterEnumeration (new ArrayEnumeration (listeners.getListenerList ())) { public boolean accept (Object o) { return o != FileChangeListener.class; } }; } } // // Refreshing the state of the object // /** Test if the file has been checked and if not, refreshes its * content. */ private final void check () { if (map == null) { refresh (null, null, false, false); if (map == null) { // create empty map to mark that we are initialized map = EMPTY; } } } /** Refresh the content of file. Ignores changes to the files provided, * instead returns its file object. * @param added do not notify addition of this file * @param removed do not notify removing of this file */ protected final void refresh (String added, String removed) { refresh (added, removed, true, false); } /** Method that allows subclasses to return its children. * * @return names (name . ext) of subfiles */ protected abstract String[] list (); /** Method to create a file object for given subfile. * @param name of the subfile * @return the file object */ protected abstract AbstractFolder createFile (String name); /** Refresh the content of file. Ignores changes to the files provided, * instead returns its file object. * @param added do not notify addition of this file * @param removed do not notify removing of this file * @param fire true if we should fire changes * @param expected true if the change has been expected by the user */ protected synchronized void refresh ( String added, String removed, boolean fire, boolean expected ) { if (isFolder ()) { // refresh of folder checks children String[] newChildren = list (); if (newChildren == null && parent == null) { // if root => we have to have children newChildren = new String[0]; } if (children == null && newChildren == null) { // no change and we are still date file return; } // System.out.println ("Refresh: " + this + " fire: " + fire); // NOI18N // Thread.dumpStack (); // new map (String, AbstractFileObject) HashMap m; // set of added files (String) Set add; // moreover map will contain only such files that disappeared int newChildrenContainNull = 0; if (newChildren != null) { int size = newChildren.length; m = new HashMap (size); add = new HashSet (size); Object replaceInSteadOfRemoved = map != null ? map.get (removed) : null; for (int i = 0; i < size; i++) { String ch = newChildren[i]; if (ch == null) { // ignore this and newChildrenContainNull++; continue; } Reference old = map == null ? null : (Reference)map.remove (ch); if (old == null) { // create new empty reference old = new WeakReference (null); add.add (ch); } m.put (ch, old); } if (added != null && replaceInSteadOfRemoved != null) { m.put (added, replaceInSteadOfRemoved); } } else { m = new HashMap (0); add = Collections.EMPTY_SET; } // maps (String, AbstractFileObject) between objects that has diappeared HashMap disappeared = null; if (fire) { // a set of files to notify that has been added if (added != null) { add.remove (added); } if (map != null) { // // MAP IS CHANGING TO (String, AbstractFileObject) // this should be fine cause map is replaced immediatelly after // this loop // disappeared = map; Iterator it = map.entrySet ().iterator (); while (it.hasNext ()) { Map.Entry entry = (Map.Entry)it.next (); // uses the value and replaces it immediatelly entry.setValue (getChild ((String)entry.getKey ())); } // remove the removed if (removed != null) { disappeared.remove (removed); } if (disappeared.isEmpty ()) { disappeared = null; } } } // use the new map // Map now contains the right content map = m; if (newChildrenContainNull != 0) { // exclude nulls from the array String[] arr = new String[newChildren.length - newChildrenContainNull]; int j = 0; for (int i = 0; i < newChildren.length; i++) { if (newChildren[i] != null) { arr[j++] = newChildren[i]; } } children = arr; } else { // ok, no nulls in the array children = newChildren; } if (fire && !add.isEmpty () && hasAtLeastOneListeners ()) { // fire these files has been added for (Iterator it = add.iterator (); it.hasNext (); ) { String name = (String)it.next (); AbstractFolder fo = getChild (name); FileEvent ev = new FileEvent (this, fo); ev.setExpected (expected); if (fo.isFolder ()) { fileCreated0 (ev, false); } else { fileCreated0 (ev, true); } // a change in children // changed = true; } } if (fire && disappeared != null && hasAtLeastOneListeners ()) { // fire these files has been removed for (Iterator it = disappeared.values ().iterator (); it.hasNext (); ) { AbstractFolder fo = (AbstractFolder)it.next (); FileEvent ev = new FileEvent (this, fo); ev.setExpected (expected); fo.fileDeleted0 (ev); // a change happened // changed = true; } } } // System.out.println ("Refresh of " + this + " ended"); // NOI18N return; } /** Constructs the full name of the object. * @return the name, e.g. <code>path/from/root.ext</code> */ public final String toString () { return getPackageNameExt ('/', '.'); } // // Serialization // public final Object writeReplace () { return new AbstractFileObject.Replace (getFileSystem ().getSystemName (), toString ()); } /** Output stream that notifies about change of the file when it is closed. */ final class NotifyOutputStream extends FilterOutputStream { private boolean closed; public NotifyOutputStream (OutputStream os) { super (os); } /** Faster implementation of writing than is implemented in * the filter output stream. */ public void write (byte[] b, int off, int len) throws IOException { out.write(b, off, len); } public void close () throws IOException { if (!closed) { closed = true; try { super.close (); } finally { fileChanged0 (new FileEvent (AbstractFolder.this)); } } } } // end of NotifyOutputStream } /* * Log * 9 Gandalf-post-FCS1.7.3.0 4/4/00 Jaroslav Tulach Faster writing to notify * output stream. Backported from Jaga. * 8 Gandalf 1.7 1/14/00 Jaroslav Tulach refresh (expected) * 7 Gandalf 1.6 1/13/00 Ian Formanek NOI18N * 6 Gandalf 1.5 1/12/00 Ian Formanek NOI18N * 5 Gandalf 1.4 1/9/00 Jaroslav Tulach When a new file is * created a fileDataCreated is fired and not fileDataChanged. * 4 Gandalf 1.3 1/5/00 Jaroslav Tulach AbstractFileSystem.refreshResource * modifies lastModified time * 3 Gandalf 1.2 11/25/99 Jaroslav Tulach List.children () can * return array that contains nulls * 2 Gandalf 1.1 11/24/99 Jaroslav Tulach FileEvent can be * expected + fired by AbstractFileSystem * 1 Gandalf 1.0 10/29/99 Jaroslav Tulach * $ * Beta Change History: * 0 Tuborg 0.32 --/--/98 Petr Hamernik isReadOnly, rename methods added * 0 Tuborg 0.33 --/--/98 Petr Hamernik getURL added * 0 Tuborg 0.34 --/--/98 Jaroslav Tulach getURL made final * 0 Tuborg 0.35 --/--/98 Petr Hamernik lock throws IOException * 0 Tuborg 0.36 --/--/98 Jaroslav Tulach comments extended * 0 Tuborg 0.38 --/--/98 Petr Hamernik file attributes * 0 Tuborg 0.39 --/--/98 Petr Hamernik comments improved * 0 Tuborg 0.40 --/--/98 Jan Formanek equals() and hashCode() added * 0 Tuborg 0.41 --/--/98 Jaroslav Tulach late fireXYZ methods, only adds to the FS fire queue * 0 Tuborg 0.42 --/--/98 Petr Hamernik URL protocol * 0 Tuborg 0.43 --/--/98 Ales Novak NbfsURLConstants */